Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extended partial methods #43582

Conversation

RikkiGibson
Copy link
Contributor

@RikkiGibson RikkiGibson commented Apr 23, 2020

Test plan: #43795
Fixes #43883 (see test ConflictingOverloads_RefOut_02)

This PR enables 'out' parameters, non-'void' returns, accessibility modifiers, and several other modifiers to be used with 'partial' methods.

Draft spec: dotnet/csharplang#3417

@RikkiGibson RikkiGibson changed the base branch from master to features/partial-methods April 23, 2020 00:07
@RikkiGibson RikkiGibson force-pushed the extended-partial-methods branch 2 times, most recently from 4b81c69 to 4d66859 Compare April 27, 2020 23:12
@RikkiGibson
Copy link
Contributor Author

At this point I believe that 'new' should not be allowed when there is no implementation part. It just seems so confusing to shadow a method with a partial that has no implementation, and then erase the call sites.

@RikkiGibson RikkiGibson marked this pull request as ready for review April 29, 2020 01:06
@RikkiGibson RikkiGibson requested a review from a team as a code owner April 29, 2020 01:06
@jaredpar
Copy link
Member

At this point I believe that 'new' should not be allowed when there is no implementation part. It just seems so confusing to shadow a method with a partial that has no implementation, and then erase the call site

Please add a PROTOTYPE to note this, or an issue in the running spec and we can follow up with LDM later.

</data>
<data name="ERR_PartialMethodWithExtendedModMustHaveImplementation" xml:space="preserve">
<value>Partial method {0} must have an implementation part because it has a 'virtual', 'override', 'sealed', or 'new', or 'extern' modifier.</value>
</data>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the LDM discussions we should not have separate errors for these violations. The requirement for an implementation is dictated by the presence of an explicit accessibility modifier. That is what mandates their is an implementation, not everything else.

At the same time all of these new scenarios must be predicated on an explicit accessibility modifier. Basically if you see out that is illegal unless there is an explicit accessibility modifier

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

<value>Both partial method declarations must be 'override' or neither may be 'override'.</value>
</data>
<data name="ERR_PartialMethodSealedDifference" xml:space="preserve">
<value>Both partial method declarations must be 'sealed' or neither may be 'sealed'.</value>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would consider just grouping all of these messages into one message. Essentially

Both partial method declarations must agree on 'override', 'virtiual', 'sealed' and accessibility

Listing out every single modifier means we have to add a new message here every time we add a feature to the langauge which impacts method signatures.

Will defer to others here if they feel there is more precedent in the other direction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the existing diagnostics have a distinct error code for 'static' mismatch, 'unsafe' mismatch and 'readonly' mismatch, but I am fine with grouping these together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

{
if (!method.ReturnType.IsVoidType())
{
diagnostics.Add(ErrorCode.ERR_PartialMethodWithNonVoidReturnMustHaveImplementation, method.Locations[0], method);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only illegal if the partial method lacks an explicit accessibility modifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}
else if (method.Parameters.Any(p => p.RefKind == RefKind.Out))
{
diagnostics.Add(ErrorCode.ERR_PartialMethodWithOutParamMustHaveImplementation, method.Locations[0], method);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only illegal if the partial method lacks an explicit accessibility modifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

}
else if (method.HasExtendedPartialModifier)
{
diagnostics.Add(ErrorCode.ERR_PartialMethodWithExtendedModMustHaveImplementation, method.Locations[0], method);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only illegal if the partial method lacks an explicit accessibility modifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, sounds like these diagnostics will change from "MustHaveImplementation" to "MustHaveAccessibility".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@@ -637,6 +641,15 @@ internal bool IsPartialWithoutImplementation
}
}

internal sealed override bool IsNew
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this is needed? My mental model is that we already error when partial methods differ an the modifier new. Hence here we're attempting to change the IsNew return to account for illegal code. Is this meant to make the IDE experience cleaner in the face of errors?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have just run myself in a circle on this. My intention was to propose making this legal to support a scenario where a user declares a partial stub which hides a base member, and the generator that produces the implementation doesn't have to think about whether to copy over the 'new' modifier or not. I'll just revert this part.

One question I have: the precedent for misuse of 'new' is to give a warning, not an error. So should the mismatch of 'new' modifiers between parts be a warning or an error?

Copy link
Contributor Author

@RikkiGibson RikkiGibson Apr 30, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now we will simply require identical combinations of 'new' modifiers (giving an error on mismatch). Can revisit if there is a compelling reason to.

@@ -756,7 +769,9 @@ internal override bool IsExpressionBodied
get { return _isExpressionBodied; }
}

private DeclarationModifiers MakeModifiers(SyntaxTokenList modifiers, MethodKind methodKind, bool hasBody, Location location, DiagnosticBag diagnostics, out bool modifierErrors)
internal bool HasExplicitAccessMod { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use Modifier instead of abbreviating as Mod in API surface area.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Diagnostic(ErrorCode.ERR_FeatureInPreview, "M1").WithArguments("extended partial methods").WithLocation(9, 32));

comp = CreateCompilation(text1, parseOptions: TestOptions.RegularWithExtendedPartialMethods);
comp.VerifyDiagnostics();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a test that verifies the DllImportAttribute is emitted correctly in this scenario?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also verify the IL for a call to M1() to ensure the call is emitted.


In reply to: 417430836 [](ancestors = 417430836)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

comp.VerifyDiagnostics();
}
}
}
Copy link
Member

@jaredpar jaredpar Apr 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel like there are several categories of tests missing here. Apologies if I read past them:

  • partial methods implementing interface members
  • partial methods which override members
  • partial methods which are both override and have nullability attributes like [MaybeNull] which impact our OHI checks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

have converted your bullet points to checkboxes :)

@RikkiGibson RikkiGibson requested a review from a team as a code owner April 29, 2020 21:33
@RikkiGibson
Copy link
Contributor Author

After examining the existing behavior for 'async partial' methods it feels like the declaration part of a method should not have 'IsExtern == true' just because the implementation uses the 'extern' keyword. So I will revert that bit of the change+tests.

@RikkiGibson RikkiGibson removed the request for review from a team April 30, 2020 21:13
@RikkiGibson
Copy link
Contributor Author

#43582 (comment)

Do we have a test for the following? Do we report "CS0663: 'Program' cannot define an overloaded method that differs only on parameter modifiers 'out' and 'ref'"?

class Program
{
    partial static void M(ref object o);
    partial static void M(out object o);
    partial static void M(out object o) { o = null; }
}

No, but PartialMethodsConsiderRefKindDifferences_RefWithOut is similar. Will add the test.

}
}
";
// PROTOTYPE: should it be allowed to specify duplicate nullability attributes?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be allowed to specify duplicate nullability attributes? [](start = 26, length = 65)

If we allow nullability attributes on both parts, we should allow attributes on both parts for all attributes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is feeling like maybe we should just ship the current rules and see how people feel about it. Adding special allowances for duplicate attributes in partials seems like something you can't just implement speculatively, we have to see how people end up using/experiencing it.

// (21,25): error CS0111: Type 'I1' already defines a member called 'M9' with the same parameter types
// static partial void M9();
Diagnostic(ErrorCode.ERR_MemberAlreadyExists, "M9").WithArguments("M9", "I1").WithLocation(21, 25),
// (22,32): error CS8796: Partial method 'I1.M10()' must have accessibility modifiers because it has a 'virtual', 'override', 'sealed', or 'new', or 'extern' modifier.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or 'new', or 'extern' [](start = 152, length = 21)

Minor: It looks like there a number of comments with the previous error text.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@@ -1682,10 +1682,13 @@ private void CheckMemberNameConflicts(DiagnosticBag diagnostics)
// UNDONE: Consider adding a secondary location pointing to the second method.
private void ReportMethodSignatureCollision(DiagnosticBag diagnostics, SourceMemberMethodSymbol method1, SourceMemberMethodSymbol method2)
{
// Partial methods are allowed to collide by signature.
if (method1.IsPartial && method2.IsPartial)
switch (method1, method2)
Copy link
Member

@cston cston May 2, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch (method1, method2) [](start = 12, length = 25)

Does this switch statement represent a change in behavior from the original if?

Unless there is a change in behavior, we should stick with the if statement since it is significantly simpler. #Closed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this fixes #43883. This solution ends up creating additional diagnostics in some error scenarios. I tried moving the checks for PartialMethodOnlyOneLatent/PartialMethodOnlyOneActual in here, but because partial method merging already happened in an earlier step, it causes us to miss diagnostics for multiple "actual" methods.

If we could do this at the same time or right before we merge partial methods, we might be able to get higher quality diagnostics in some scenarios when members have conflicting signatures:

  1. if the methods are not 'partial', we report ErrorCode.ERR_MemberAlreadyExists
  2. if we detect that both methods are partial implementations, we give ErrorCode.ERR_PartialMethodOnlyOneActual
  3. if we detect that both methods are partial definitions, we give ErrorCode.ERR_PartialMethodOnlyOneLatent

It might also be possible to fix this by modifying the partial method signature comparer, that groups declarations that might be part of the same partial method, to be aware of RefKind differences.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This solution ends up creating additional diagnostics in some error scenarios.

That sounds ok. We can revisit this later if necessary.


In reply to: 418977822 [](ancestors = 418977822)

// public delegate System.Collections.Generic.IAsyncEnumerable<int> Delegate([EnumeratorCancellation] CancellationToken token); // 4
Diagnostic(ErrorCode.WRN_UnconsumedEnumeratorCancellationAttributeUsage, "EnumeratorCancellation").WithArguments("token").WithLocation(15, 80),
// (16,62): error CS0766: Partial methods must have a void return type
// (16,62): error CS9051: Partial method 'C2.M(CancellationToken)' must have accessibility modifiers because it has a non-void return type.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CS9051 [](start = 34, length = 6)

There are a number of instances of CS9051.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

parseOptions: TestOptions.RegularWithExtendedPartialMethods,
expectedOutput: "1");
verifier.VerifyDiagnostics();
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

} [](start = 8, length = 1)

Consider testing access outside the type hierarchy as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added more accessibility tests.

@RikkiGibson
Copy link
Contributor Author

@jaredpar please let me know if I have addressed all your comments.

Copy link
Member

@jaredpar jaredpar left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some more test suggestions but overall looking good.

}

[Fact]
public void Override_AllowNull_02()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is there a test where we have a override partial which involves [AllowNull] that succeeds?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand. Override_AllowNull_02 has the expected combination of [AllowNull] given on the parameters of the override, and it warns on dereference of the parameters. That seems successful.

comp.VerifyDiagnostics();
}

[Fact]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider an override test where the types change from generic parameters to actual types. Example:

class Base<T> {
  protected virtual void M(T p) { } 
}

partial class Derived : Base<string> {
  protected override partial void M(string p); 
  protected override partial void M(string p) { }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding to the test plan and will address in a follow-up PR.

@RikkiGibson RikkiGibson merged commit 5bc831c into dotnet:features/partial-methods May 5, 2020
@RikkiGibson RikkiGibson deleted the extended-partial-methods branch May 5, 2020 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants